﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using WCSFContrib.PageFlow.Xml.Configuration;
using System.Globalization;
using System.Reflection;
using WCSFContrib.PageFlow.Xml.Properties;
using System.Xml;
using System.Xml.XPath;
using System.Web;

namespace WCSFContrib.PageFlow.Xml.Services
{
    public class DistributedWebConfigStore
    {
        private string _baseDirectory;

        public DistributedWebConfigStore()
            : this(String.Empty)
        {
        }

        public DistributedWebConfigStore(string baseDirectory)
        {
            _baseDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, baseDirectory);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        public IDictionary<string, NavigationGraph> GetXmlPageFlowNavigationGraphs()
        {
            List<string> configFiles = new List<string>();
            DirectoryInfo BaseAppDir = new DirectoryInfo(_baseDirectory);

            DirectoryInfo[] ModuleDirs = BaseAppDir.GetDirectories();

            foreach (DirectoryInfo dir in ModuleDirs)
           { 
                string[] files = Directory.GetFiles(dir.FullName, "*.Config", SearchOption.AllDirectories);
                configFiles.AddRange(files);
            }

            XmlPageFlowConfigurationSection configSection = GetDistributedConfiguration(
                Path.Combine(_baseDirectory, "Web.Config"), configFiles.ToArray());

            return PopulateSection(configSection);
        }

        public string GetFullyQualifiedViewKey(string viewname, string module)
        {
            if (string.IsNullOrEmpty(viewname))
                return viewname;

            if (string.IsNullOrEmpty(module) || viewname.Contains("."))
                return viewname;

            return string.Format("{0}.{1}", module, viewname);
        }

        private IDictionary<string, NavigationGraph> PopulateSection(XmlPageFlowConfigurationSection configSection)
        {
            IDictionary<string, View> temporalViews = new Dictionary<string, View>(configSection.Views.Count);
            IDictionary<string, NavigationGraph> navigationGraphs = new Dictionary<string, NavigationGraph>(new EqualityLowerComparer<string>());

            // if the config section has asked for the sitemap to provide the views
            if (configSection.UseSitemapViews)
            {
                SiteMapNodeCollection nodes = SiteMap.RootNode.GetAllNodes();

                foreach (SiteMapNode node in nodes)
                {
                    View view = new View(node.Key, node.Url);
                    temporalViews.Add(view.Name, view);
                }
            }
            else
            {
                foreach (ViewConfigurationElement configView in configSection.Views)
                {
                    View view = new View(configView.Name, configView.Type);

                    temporalViews.Add(view.Name, view);
                }
            }

            foreach (NavigationGraphConfigurationElement configNavGraph in configSection.NavigationGraphs)
            {
                IXmlPageFlowInstanceStore xmlPageFlowInstanceStore = GetInstanceStore(configNavGraph.StatePersist, configSection.ObjectTypes.StatePersistenceCollection);
                Type xmlPageFlowType = GetPageFlowType(configNavGraph.PageFlowType, configSection.ObjectTypes.PageFlowTypesCollection);

                string startView = GetFullyQualifiedViewKey(configNavGraph.StartView, configNavGraph.Module);
                string abortView = GetFullyQualifiedViewKey(configNavGraph.AbortView, configNavGraph.Module);
                string notRunningView = GetFullyQualifiedViewKey(configNavGraph.NotRunningView, configNavGraph.Module);

                if(!string.IsNullOrEmpty(abortView)) abortView = temporalViews[abortView].Url;
                if (!string.IsNullOrEmpty(notRunningView)) notRunningView = temporalViews[notRunningView].Url;

                NavigationGraph navigationGraph = new NavigationGraph(configNavGraph.Name,
                                                       xmlPageFlowInstanceStore,
                                                       temporalViews[startView],
                                                       abortView,
                                                       configNavGraph.Abandonable,
                                                       notRunningView,
                                                       xmlPageFlowType);

                navigationGraph.Constrained = configSection.AllowBackButton;

                foreach (NodeConfigurationElementCollection configNode in configNavGraph)
                {
                    View view = temporalViews[GetFullyQualifiedViewKey(configNode.View, configNavGraph.Module)];

                    if (view != null && !navigationGraph.Views.Contains(view))
                        navigationGraph.Views.Add(view);

                    foreach (NavigateToConfigurationElement configNavigateTo in configNode)
                    {
                        if (String.IsNullOrEmpty(configNavigateTo.View) && String.IsNullOrEmpty(configNavigateTo.Condition))
                            throw new ConfigurationErrorsException(String.Format(CultureInfo.CurrentCulture, Resources.ConfigurationInvalidTransition, configNavigateTo.NavigateValue));

                        View transitionView = null;

                        string navigateToView;

                        if (string.IsNullOrEmpty(configNavGraph.Module) || configNavigateTo.View.Contains("."))
                            navigateToView = configNavigateTo.View;
                        else
                            navigateToView = string.Format("{0}.{1}", configNavGraph.Module, configNavigateTo.View);

                        if (temporalViews.ContainsKey(navigateToView))
                            transitionView = temporalViews[navigateToView];

                        string conditionName = (String.IsNullOrEmpty(configNavigateTo.Condition)) ? null : configNavigateTo.Condition;

                        if (view != null)
                            view.Transitions.Add(new Transition(configNavigateTo.NavigateValue, transitionView, conditionName));

                        if (transitionView != null && !navigationGraph.Views.Contains(transitionView))
                            navigationGraph.Views.Add(transitionView);
                    }
                }

                navigationGraphs.Add(navigationGraph.PageFlowType.AssemblyQualifiedName, navigationGraph);
            }

            return navigationGraphs;
        }

        /// <summary>
        /// Build up a configuration section based in multiple files, appending rather than rewriting of sections appearing in two files
        /// </summary>
        /// <param name="siteConfigFile">The web config file for the site, contains provider settings</param>
        /// <returns></returns>
        private static XmlPageFlowConfigurationSection GetDistributedConfiguration(string siteConfigFile, string[] configFilePathList)
        {
            XmlPageFlowConfigurationSection configuration;

            // get our initial web config, with provider settings and possibly data. Hopefully we are ignoring the other web configs
            configuration = (XmlPageFlowConfigurationSection)GetConfigurationForCustomFile(siteConfigFile).GetSection("pageFlow/xmlPageFlow");

            // parse each "new" file to get the node graph info etc and stick it into the configuration file
            // this way we wont need to interfere any more after this point
            foreach (string configPath in configFilePathList)
            {
                XmlDocument configDocument = new XmlDocument();
                configDocument.Load(configPath);

                XPathNavigator nav = configDocument.CreateNavigator();

                AssemblyGraphConfigurationElement[] assemblies = GetModuleEmbeddedPageFlows(nav);

                foreach (AssemblyGraphConfigurationElement assembly in assemblies)
                {
                    Assembly asm = Assembly.Load(assembly.Assembly);

                    Stream s = asm.GetManifestResourceStream(assembly.ResourcePath);


                    XmlDocument asmDocument = new XmlDocument();
                    if (s != null) asmDocument.Load(s);
                    XPathNavigator asmNav = asmDocument.CreateNavigator();

                    LoadNavigationGraphs(asmDocument, asmNav, ref configuration);
                    LoadViews(asmDocument, asmNav, ref configuration);
                }

                // load the documents config data into the XmlPageFlowConfigurationSection
                LoadNavigationGraphs(configDocument, nav, ref configuration);
                LoadViews(configDocument, nav, ref configuration);
                LoadPageFlowTypes(configDocument, nav, ref configuration);
            }

            return configuration;
        }

        private static AssemblyGraphConfigurationElement[] GetModuleEmbeddedPageFlows(XPathNavigator nav)
        {
            XmlNamespaceManager ns = new XmlNamespaceManager(nav.NameTable);
            XPathExpression getAssembliesExpression = nav.Compile("configuration/pageFlow/xmlPageFlow/navigationGraphs/assemblyGraph");
            XPathNodeIterator iter = nav.Select(getAssembliesExpression);

            List<AssemblyGraphConfigurationElement> elements = new List<AssemblyGraphConfigurationElement>();

            while (iter.MoveNext())
            {
                AssemblyGraphConfigurationElement assembly = new AssemblyGraphConfigurationElement();

                assembly.Assembly = iter.Current.GetAttribute("assembly", ns.DefaultNamespace);
                assembly.ResourcePath = iter.Current.GetAttribute("resourcePath", ns.DefaultNamespace);

                elements.Add(assembly);
            }

            return elements.ToArray();
        }

        /// <summary>
        /// Loads an XmlDocuments PageFlowObjectTypes into the provided XmlPageFlowConfigurationSection
        /// </summary>
        /// <param name="configDocument">The document to parse</param>
        /// <param name="nav">The documents Xpath navigator</param>
        /// <param name="configuration">The config section to load the data into</param>
        private static void LoadPageFlowTypes(XmlNode configDocument, XPathNavigator nav, ref XmlPageFlowConfigurationSection configuration)
        {
            XmlNodeList nodes = configDocument.SelectNodes("configuration/pageFlow/xmlPageFlow/objectTypes/pageFlowType");

            if (nodes != null)
                foreach (XmlNode node in nodes)
                {
                    PageFlowTypeSettings pageFlowSettings = new PageFlowTypeSettings(node);
                    configuration.ObjectTypes.PageFlowTypesCollection.Add(pageFlowSettings.Name, pageFlowSettings);
                }
        }

        /// <summary>
        /// Loads an XmlDocuments Views into the provided XmlPageFlowConfigurationSection
        /// </summary>
        /// <param name="configDocument">The document to parse</param>
        /// <param name="nav">The documents Xpath navigator</param>
        /// <param name="configuration">The config section to load the data into</param>
        private static void LoadViews(XmlDocument configDocument, XPathNavigator nav, ref XmlPageFlowConfigurationSection configuration)
        {
            // views
            XmlNamespaceManager ns = new XmlNamespaceManager(nav.NameTable);
            XPathExpression getViewsExpression = nav.Compile("configuration/pageFlow/xmlPageFlow/views/view");

            XPathNodeIterator iter = nav.Select(getViewsExpression);

            while (iter.MoveNext())
            {
                ViewConfigurationElement viewConfigElement = new ViewConfigurationElement();
                viewConfigElement.Name = iter.Current.GetAttribute("name", ns.DefaultNamespace);
                viewConfigElement.Type = iter.Current.GetAttribute("type", ns.DefaultNamespace);

                configuration.Views.Add(viewConfigElement);
            }
        }

        /// <summary>
        /// Loads an XmlDocuments Navigation Graphs into the provided XmlPageFlowConfigurationSection
        /// </summary>
        /// <param name="configDocument">The document to parse</param>
        /// <param name="nav">The documents Xpath navigator</param>
        /// <param name="configuration">The config section to load the data into</param>
        private static void LoadNavigationGraphs(XmlDocument configDocument, XPathNavigator nav, ref XmlPageFlowConfigurationSection configuration)
        {
            // navigation graphs
            XmlNamespaceManager ns = new XmlNamespaceManager(nav.NameTable);
            XPathExpression getNavGraphsExpression = nav.Compile("configuration/pageFlow/xmlPageFlow/navigationGraphs/navigationGraph");
            XPathNodeIterator iter = nav.Select(getNavGraphsExpression);

            while (iter.MoveNext())
            {
                NavigationGraphConfigurationElement element = new NavigationGraphConfigurationElement();

                element.Name = iter.Current.GetAttribute("name", ns.DefaultNamespace);
                element.StatePersist = iter.Current.GetAttribute("statePersist", ns.DefaultNamespace);
                element.StartView = iter.Current.GetAttribute("startView", ns.DefaultNamespace);

                element.Abandonable = (Microsoft.Practices.PageFlow.AbandonBehavior)Enum.Parse(typeof(Microsoft.Practices.PageFlow.AbandonBehavior), iter.Current.GetAttribute("abandonable", ns.DefaultNamespace));
                element.AbortView = iter.Current.GetAttribute("abortView", ns.DefaultNamespace);
                element.PageFlowType = iter.Current.GetAttribute("pageFlowType", ns.DefaultNamespace);
                element.NotRunningView = iter.Current.GetAttribute("notRunningView", ns.DefaultNamespace);
                element.Module = iter.Current.GetAttribute("module", ns.DefaultNamespace);

                XPathNodeIterator nodeIter = iter.Current.SelectChildren("node", ns.DefaultNamespace);

                while (nodeIter.MoveNext())
                {
                    NodeConfigurationElementCollection collection = new NodeConfigurationElementCollection();
                    collection.View = nodeIter.Current.GetAttribute("view", ns.DefaultNamespace);

                    XPathNodeIterator navToIter = nodeIter.Current.SelectChildren("navigateTo", ns.DefaultNamespace);

                    while (navToIter.MoveNext())
                    {
                        NavigateToConfigurationElement navToElement = new NavigateToConfigurationElement();

                        navToElement.NavigateValue = navToIter.Current.GetAttribute("navigateValue", ns.DefaultNamespace);
                        navToElement.View = navToIter.Current.GetAttribute("view", ns.DefaultNamespace);
                        navToElement.Condition = navToIter.Current.GetAttribute("condition", ns.DefaultNamespace);

                        collection.Add(navToElement);
                    }

                    element.Add(collection);
                }

                // load our fresh parsed navigation graph into the configuration file
                configuration.NavigationGraphs.Add(element);
            }
        }

        private static System.Configuration.Configuration GetConfigurationForCustomFile(string fileName)
        {
            ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
            fileMap.ExeConfigFilename = fileName;
            File.SetAttributes(fileMap.ExeConfigFilename, FileAttributes.Normal);
            return ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
        }


        private IDictionary<string, IXmlPageFlowInstanceStore> instanceStores = new Dictionary<string, IXmlPageFlowInstanceStore>();
        /// <summary>
        /// Lazy load the persistence stores as navigation graphs that uses them appear in the configuration
        /// </summary>
        private IXmlPageFlowInstanceStore GetInstanceStore(string name, Dictionary<string, StatePersistenceProviderSettings> settings)
        {
            if (instanceStores.ContainsKey(name))
                return instanceStores[name];

            StatePersistenceProviderSettings storeSettings = settings[name];
            if (storeSettings.Type == null)
                throw new ConfigurationErrorsException(String.Format(CultureInfo.CurrentCulture, Resources.InvalidConfigurationTypeValue, storeSettings.Name));

            IXmlPageFlowInstanceStore store;
            try
            {
                store = (IXmlPageFlowInstanceStore)Activator.CreateInstance(storeSettings.Type, storeSettings.AdditionalAttributes);
            }
            catch (MissingMethodException)
            {
                store = (IXmlPageFlowInstanceStore)Activator.CreateInstance(storeSettings.Type);
            }
            instanceStores.Add(storeSettings.Name, store);

            return store;
        }

        private static Type GetPageFlowType(string name, Dictionary<string, PageFlowTypeSettings> settings)
        {
            PageFlowTypeSettings pageFlowTypeSettings = settings[name];

            if (pageFlowTypeSettings.Type == null)
                throw new ConfigurationErrorsException(String.Format(CultureInfo.CurrentCulture, Resources.InvalidConfigurationPageFlowTypeValue, pageFlowTypeSettings.Name));

            return pageFlowTypeSettings.Type;
        }
    }
}
